package cn.com.easy.autocomplete.service;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.PostConstruct;

import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.Index.Duplicates;
import org.springframework.data.mongodb.core.index.TextIndexDefinition;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Order;
import org.springframework.data.mongodb.core.query.Query;

import cn.com.easy.autocomplete.model.WordModel;
import cn.com.easy.autocomplete.utils.PinyinUtils;

/**
 * 搜索词按首字母分库，服务类
 * 
 * @author nibili 2015年6月19日
 * 
 */
@SuppressWarnings({ "deprecation", "unused" })
public class PartitionWordService {

	private static Logger logger = LoggerFactory.getLogger(PartitionWordService.class);
	private static Random random = new Random(System.currentTimeMillis());
	/** 拼音处理类 */
	private static HanyuPinyinOutputFormat outputFormat = new HanyuPinyinOutputFormat();
	static {
		// 不需要声调
		outputFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
		// 遇到“ü” 显示成V
		outputFormat.setVCharType(HanyuPinyinVCharType.WITH_V);
		// 所有输出大写
		outputFormat.setCaseType(HanyuPinyinCaseType.UPPERCASE);
	}
	/** 搜索词分库依据的字母开头 */
	private static final String[] COLLECTION_NAME_POSTFIX = new String[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
			"Z", "1" };
	/** 集合名称前缀 */
	private static final String COLLECTION_NAME_PATTERN = "WORD_%1$s";
	/** 字母识别正则表达式 */
	private static final String LETTER_REGEX = "[a-zA-Z]+";
	/** 使用次数降序排 */
	private static final Sort SORT_BY_USECOUNT_DESC = new Sort(Direction.DESC, "useCount");
	/** 结果集大小 降序排 */
	private static final Sort SORT_BY_RESULTCOUNT_DESC = new Sort(Direction.DESC, "resultCount");
	/** 27个集合的map */
	private Map<String, String> collectionsNameMap = new ConcurrentHashMap<String, String>(27);

	/** mongodb操作类 */
	@Autowired
	private MongoTemplate mongoOperations;

	/**
	 * 服务初始化
	 * 
	 * @auth nibili 2015年6月19日
	 */
	@PostConstruct
	public void init() {
		// 初始化字母开头的 集合，26个字母，加一个数字开头的
		for (String letter : COLLECTION_NAME_POSTFIX) {
			String collectionName = String.format(COLLECTION_NAME_PATTERN, letter);
			if (!mongoOperations.collectionExists(collectionName)) {
				createWordCollection(collectionName);
				ensureIndex(collectionName);
			}
			collectionsNameMap.put(letter, collectionName);
		}
		collectionsNameMap = Collections.unmodifiableMap(collectionsNameMap);
	}

	/**
	 * 重建 分库集合
	 * 
	 * @auth nibili 2015年6月19日
	 */
	public void recreateWordCollections() {
		for (Map.Entry<String, String> entry : collectionsNameMap.entrySet()) {
			// 删除一个分库集合
			dropWordCollection(entry.getValue());
			// 创建一个分库集合
			createWordCollection(entry.getValue());
			// 创建分库字段索引
			ensureIndex(entry.getValue());
		}
	}

	/**
	 * 保存关键字
	 * 
	 * @param text
	 * @param useCount
	 * @param resultCount
	 * @auth nibili 2015年6月19日
	 */
	public void saveWord(String text) {
		saveWord(text, 0, 0);
	}

	/**
	 * 保存关键字
	 * 
	 * @param text
	 * @param useCount
	 *            使用次数
	 * @param resultCount
	 *            总结果数
	 * @auth nibili 2015年6月19日
	 */
	public void saveWord(String text, int useCount, int resultCount) {
		WordModel word = new WordModel();
		String pinyin = PinyinUtils.getPinYin(text.trim().toLowerCase(), "");
		word.setInitials(PinyinUtils.getInitials(text));
		word.setPinyin(pinyin);
		word.setResultCount(resultCount);
		word.setText(text);
		word.setUseCount(useCount);
		this.saveWord(word);
	}

	/**
	 * 保存 搜索词
	 * 
	 * @param WordModel
	 * @auth nibili 2015年6月19日
	 */
	public void saveWord(WordModel wordModel) {
		if (StringUtils.isEmpty(wordModel.getText()) || StringUtils.isEmpty(wordModel.getPinyin()) || StringUtils.isEmpty(wordModel.getInitials())) {
			throw new IllegalArgumentException("数据不合法，文本或拼音或拼音缩写至少有一项为空");
		}

		String splitKey = getSplitKey(wordModel.getText());
		logger.debug("text:{},splitKey:{}", wordModel.getText(), splitKey);
		String collectionName = collectionsNameMap.get(splitKey);
		if (wordModel.getId() == null) {

			try {
				String text = wordModel.getText();
				Query query = new Query();
				Criteria criteria = new Criteria();
				criteria.and("text").is(text);
				query.addCriteria(criteria);
				query.limit(1);
				WordModel wordModelTemp = mongoOperations.findOne(query, WordModel.class, collectionName);
				if (wordModelTemp == null) {
					// 还没有这个词
					String generatedId = splitKey + RandomStringUtils.random(7, true, true);
					// 判断id是否已存在
					while (mongoOperations.findById(generatedId, WordModel.class, collectionName) != null) {
						generatedId = splitKey + RandomStringUtils.random(7, true, true);
					}
					// 最终取到一个不重复的id
					wordModel.setId(generatedId);
				} else {
					// 已有这个词
					wordModel.setId(wordModelTemp.getId());
				}
			} catch (Exception e) {
				logger.error("生成id出现异常", e);
				// 为避免冲突的时候数据库查询太快，随机休眠200ms左右
				try {
					Thread.sleep(20 * random.nextInt(10));
				} catch (InterruptedException ie) {
				}
			}
		}
		mongoOperations.save(wordModel, collectionName);
		logger.debug("id:{},collection:{}", wordModel.getId(), collectionName);
	}

	// /**
	// * id 查找搜索词
	// *
	// * @param id
	// * @return
	// * @auth nibili 2015年6月19日
	// */
	// public wordModel findById(String id) {
	// return mongoOperations.findById(id, wordModel.class,
	// getPartitionCollectionName(id));
	// }

	/**
	 * 按 搜索词字段查找 实体
	 * 
	 * @param text
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	public WordModel findByText(String text) {
		return mongoOperations.findOne(new Query().addCriteria(Criteria.where("text").is(text)), WordModel.class, getPartitionCollectionName(text));
	}

	// /**
	// * 要据id，删除实体
	// *@param id
	// * @auth nibili 2015年6月19日
	// */
	// public void removeById(String id) {
	// String collectionName = getPartitionCollectionName(id);
	// WordModel wordModel = mongoOperations.findById(id, WordModel.class,
	// collectionName);
	// if (WordModel != null) {
	// mongoOperations.remove(WordModel, collectionName);
	// }
	// }
	/**
	 * 按搜索词，删掉实体
	 * 
	 * @param text
	 * @auth nibili 2015年6月19日
	 */
	public void removeByText(String text) {
		mongoOperations.remove(new Query().addCriteria(Criteria.where("text").is(text)), getPartitionCollectionName(text));
	}

	/**
	 * 按实体结果，删除实体
	 * 
	 * @param WordModel
	 * @auth nibili 2015年6月19日
	 */
	public void remove(WordModel wordModel) {
		mongoOperations.remove(wordModel, collectionsNameMap.get(getSplitKey(wordModel.getText())));
	}

	/**
	 * 按搜索词 查找 相关结果<br>
	 * 选判断是否是字母，<br>
	 * 是：查找拼音开头--》查找缩写词开头--》文本匹配<br>
	 * 否：按文本字段查
	 * 
	 * @param text
	 * @param limit
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	public List<WordModel> findWordsLike(String text, int limit) {
		String collectionName = getPartitionCollectionName(text);
		List<WordModel> words = null;
		// 是否全部是字母
		if (text.matches(LETTER_REGEX)) {
			text = text.toLowerCase();
			// 查找拼音开头的
			words = findWordsByPinyinLike(text, limit, collectionName);
			if (words == null || words.size() == 0) {
				// 查找缩写词开头
				words = findWordsByInitialsLike(text, limit, collectionName);
			}
			if (words == null || words.size() == 0) {
				// 文本匹配
				words = findWordsByTextLike(text, limit, collectionName);
			}
		} else {
			// 文本匹配
			words = findWordsByTextLike(text, limit, collectionName);
		}
		return words;
	}

	/**
	 * 查找拼音全匹配的词
	 * 
	 * @param text
	 * @param limit
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	public List<WordModel> findWordsWithSamePinyin(String text, int limit) {
		String collectionName = getPartitionCollectionName(text);
		text = PinyinUtils.getPinYin(text.trim().toLowerCase(), "");
		return mongoOperations.find(new Query().addCriteria(Criteria.where("pinyin").regex("^" + text + "@")).with(SORT_BY_RESULTCOUNT_DESC).limit(limit), WordModel.class, collectionName);
	}

	/**
	 * 查找拼音开头的
	 * 
	 * @param pinyin
	 * @param limit
	 * @param collectionName
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	private List<WordModel> findWordsByPinyinLike(String pinyin, int limit, String collectionName) {
		return mongoOperations.find(new Query().addCriteria(Criteria.where("pinyin").regex("^" + pinyin)).with(SORT_BY_USECOUNT_DESC).limit(limit), WordModel.class, collectionName);
	}

	/**
	 * 查找缩写词开头的
	 * 
	 * @param pinyin
	 * @param limit
	 * @param collectionName
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	private List<WordModel> findWordsByInitialsLike(String pinyin, int limit, String collectionName) {
		return mongoOperations.find(new Query().addCriteria(Criteria.where("initials").regex("^" + pinyin)).with(SORT_BY_USECOUNT_DESC).limit(limit), WordModel.class, collectionName);
	}

	/**
	 * 文本开头的
	 * 
	 * @param text
	 * @param limit
	 * @param collectionName
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	private List<WordModel> findWordsByTextLike(String text, int limit, String collectionName) {
		return mongoOperations.find(new Query().addCriteria(Criteria.where("text").regex("^" + text)).with(SORT_BY_USECOUNT_DESC).limit(limit), WordModel.class, collectionName);
	}

	/**
	 * 分页获取和text在同一个集合的所有Word
	 * 
	 * @param text
	 * @param pageRequest
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	public Page<WordModel> findAllWordsInSameCollection(String text, PageRequest pageRequest) {
		Query query = new Query();
		String collectionName = getPartitionCollectionName(text);
		long count = mongoOperations.count(query, collectionName);
		List<WordModel> words = mongoOperations.find(query.with(pageRequest), WordModel.class, collectionName);
		return new PageImpl<WordModel>(words, pageRequest, count);
	}

	/**
	 * 分页获取，某个分表的的数据
	 * 
	 * @param pageRequest
	 *            分页请求
	 * @param collectionName
	 *            集合名
	 * @return
	 */
	public List<WordModel> findWordsByPageInCollection(PageRequest pageRequest, String collectionName) {
		return mongoOperations.find(new Query().with(pageRequest), WordModel.class, collectionName);
	}

	/**
	 * 不分页，获取，某个分表的所有数据
	 * 
	 * @param collectionName
	 *            集合名
	 */
	public List<WordModel> findAllWordsInCollection(String collectionName) {
		return mongoOperations.findAll(WordModel.class, collectionName);
	}

	/**
	 * 获取某个分表的数据总和
	 * 
	 * @param collectionName
	 *            集合名
	 */
	public long countWordsInCollection(String collectionName) {
		return mongoOperations.count(new Query(), collectionName);
	}

	/**
	 * 获取当前分库集合
	 * 
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	public Map<String, String> getCollectionsNameMap() {
		return collectionsNameMap;
	}

	/**
	 * 按搜索词，获取分库集合名称
	 * 
	 * @param text
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	private String getPartitionCollectionName(String text) {
		return collectionsNameMap.get(getSplitKey(text));
	}

	/**
	 * 按搜索词，确定 分库集合 的 后缀
	 * 
	 * @param text
	 * @return
	 * @auth nibili 2015年6月19日
	 */
	private String getSplitKey(String text) {
		int charValue = (int) text.toUpperCase().charAt(0);
		if (47 < charValue && charValue < 58) {
			// 是数字
			return "1";
		} else if (64 < charValue && charValue < 91) {
			// 判断是字母
			return String.valueOf((char) charValue);
		} else if (19967 < charValue && charValue < 40870) {
			// 是中文，unicode范围：[\u4E00-\u9FA5]
			try {
				return String.valueOf(PinyinHelper.toHanyuPinyinStringArray((char) charValue, outputFormat)[0].charAt(0));
			} catch (BadHanyuPinyinOutputFormatCombination e) {
				logger.error(e.getMessage(), e);
				throw new RuntimeException("汉字转拼音出现异常，文本内容：" + text);
			}
		} else {
			throw new IllegalArgumentException("首字母非数字、字母或汉字，文本内容：" + text);
		}
	}

	/**
	 * 删除一个分库集合
	 * 
	 * @param collectionName
	 * @auth nibili 2015年6月19日
	 */
	private void dropWordCollection(String collectionName) {
		logger.info("删除集合{}", collectionName);
		mongoOperations.dropCollection(collectionName);
	}

	/**
	 * 创建一个分库集合
	 * 
	 * @param collectionName
	 * @auth nibili 2015年6月19日
	 */
	private void createWordCollection(String collectionName) {
		logger.info("创建集合{}", collectionName);
		mongoOperations.createCollection(collectionName);
	}

	/**
	 * 创建分库字段索引
	 * 
	 * @param collectionName
	 * @auth nibili 2015年6月19日
	 */
	private void ensureIndex(String collectionName) {
		logger.info("为集合{}确认索引", collectionName);
		// mongoOperations.indexOps(collectionName).ensureIndex(TextIndexDefinition.builder().named("initialsIndex").onAllFields().build());
		// mongoOperations.indexOps(collectionName).ensureIndex(TextIndexDefinition.builder().named("pinyinIndex").onAllFields().build());
		// mongoOperations.indexOps(collectionName).ensureIndex(TextIndexDefinition.builder().named("textIndex").onAllFields().build());
		mongoOperations.indexOps(collectionName).ensureIndex(new Index("initials", Order.ASCENDING).named("initialsIndex"));
		mongoOperations.indexOps(collectionName).ensureIndex(new Index("pinyin", Order.ASCENDING).named("pinyinIndex"));
		mongoOperations.indexOps(collectionName).ensureIndex(new Index("text", Order.ASCENDING).named("textIndex").unique(Duplicates.DROP));
		mongoOperations.indexOps(collectionName).ensureIndex(new Index("useCount", Order.DESCENDING).named("useCountIndex"));
		mongoOperations.indexOps(collectionName).ensureIndex(new Index("resultCount", Order.DESCENDING).named("resultCountIndex"));
	}

}
